package com.michaelbulava.TFS.vcs.internal.providers.internal;

import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.vcsUtil.VcsUtil;
import com.michaelbulava.TFS.Messages;
import com.michaelbulava.TFS.vcs.TFSVcs;
import com.michaelbulava.TFS.vcs.exceptions.TFSException;
import com.michaelbulava.TFS.vcs.revision.TFSContentRevision;
import com.michaelbulava.TFS.vcs.utils.TFSFileUtil;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.*;
import com.microsoft.tfs.core.clients.versioncontrol.specs.ItemSpec;
import com.microsoft.tfs.core.clients.versioncontrol.specs.version.ChangesetVersionSpec;
import com.microsoft.tfs.util.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

/**
 * Created by MichaelA on 8/17/2015.
 */
public class TFSChangeList implements CommittedChangeList {
    private Workspace myWorkspace;
    private final TFSVcs myVcs;
    private int myRevisionNumber;
    private String myAuthor;
    private Date myDate;
    private String myComment;
    private List<Change> myCachedChanges;
    private final Map/*<FilePath, Pair<FilePath, Integer>>*/ myModifiedPaths;
    private final Set<FilePath> myAddedPaths;
    private final Map<FilePath, Integer> myDeletedPaths;
    private final Map/*<FilePath, Pair<FilePath, Integer>>*/ myMovedPaths;
    private URI myServerUri;
    private String myWorkspaceName;

    public TFSChangeList(TFSVcs vcs, DataInput stream){
        myModifiedPaths = new HashMap();
        myAddedPaths = new HashSet();
        myDeletedPaths = new HashMap();
        myMovedPaths = new HashMap();
        myVcs = vcs;
        readFromStream(stream);
    }

    public TFSChangeList(Workspace workspace, int revisionNumber, String author, Date date, String comment,
                         TFSVcs vcs) {
        myModifiedPaths = new HashMap();
        myAddedPaths = new HashSet();
        myDeletedPaths = new HashMap();
        myMovedPaths = new HashMap();
        myWorkspace = workspace;
        myRevisionNumber = revisionNumber;
        myAuthor = author;
        myDate = date;
        myComment = comment == null ? "" : comment;
        myVcs = vcs;
        myWorkspaceName = myWorkspace.getName();
        myServerUri = myWorkspace.getServerURI();
    }

    @Override
    public String getCommitterName() {
        return myAuthor;
    }

    @Override
    public Date getCommitDate() {
        return myDate;
    }

    @Override
    public long getNumber() {
        return myRevisionNumber;
    }

    @Nullable
    @Override
    public String getBranch() {
        return null;
    }

    @Override
    public AbstractVcs getVcs() {
        return myVcs;
    }

    @Override
    public Collection<Change> getChangesWithMovedTrees() {
        return getChanges();
    }

    @Override
    public boolean isModifiable() {
        return true;
    }

    @Override
    public void setDescription(String description) {
        myComment = description;
    }

    @Override
    public Collection<Change> getChanges() {
        if (myCachedChanges == null){
            try{
                if(myWorkspace != null)
                    loadChanges();
                myCachedChanges = new ArrayList();
                FilePath path;
                for(Iterator<FilePath> i$ = myAddedPaths.iterator(); i$.hasNext(); myCachedChanges.add(new Change(null,
                        TFSContentRevision.create(myVcs.getProject(), path, myRevisionNumber)))){
                    path = i$.next();
                }


                Map.Entry/*<FilePath, Integer>*/ entry;
                for(Iterator i$ = myDeletedPaths.entrySet().iterator(); i$.hasNext();
                    myCachedChanges.add(new Change(TFSContentRevision.create(myVcs.getProject(),
                            (FilePath)entry.getKey(), ((Integer)entry.getValue()).intValue()), null)))
                    entry = (Map.Entry) i$.next();

                TFSContentRevision beforeRevision;
                TFSContentRevision afterRevision;
                for(Iterator<Map.Entry<FilePath, Integer>> i$ = myModifiedPaths.entrySet().iterator(); i$.hasNext();
                    myCachedChanges.add(new Change(beforeRevision, afterRevision))) {
                    entry = (java.util.Map.Entry)i$.next();
                    beforeRevision = TFSContentRevision.create(myVcs.getProject(), (FilePath)entry.getKey(),
                            ((Integer)entry.getValue()).intValue());
                    afterRevision = TFSContentRevision.create(myVcs.getProject(), (FilePath)entry.getKey(),
                            myRevisionNumber);
                }

                for(Iterator i$ = myMovedPaths.entrySet().iterator(); i$.hasNext();
                    myCachedChanges.add(new Change(null, afterRevision))) {
                    entry = (java.util.Map.Entry)i$.next();
                    beforeRevision = TFSContentRevision.create(myVcs.getProject(), (FilePath)entry.getKey(),
                            ((Integer)((Pair)entry.getValue()).second).intValue());
                    afterRevision = TFSContentRevision.create(myVcs.getProject(),
                            (FilePath)((Pair)entry.getValue()).first, myRevisionNumber);
                    myCachedChanges.add(new Change(beforeRevision, null));
                }

            } catch (TFSException e) {
                AbstractVcsHelper.getInstance(myVcs.getProject()).showError(new VcsException(e.getMessage(), e),
                        TFSVcs.VCS_NAME);
            }
        }
        return myCachedChanges;
    }

    @NotNull
    @Override
    public String getName() {
        return myComment;
    }

    @Override
    public String getComment() {
        return myComment;
    }

    public void writeToStream(DataOutput stream) throws IOException {
        stream.writeUTF(myServerUri.toString());
        stream.writeUTF(myWorkspaceName);
        stream.writeInt(myRevisionNumber);
        stream.writeUTF(myAuthor);
        stream.writeLong(myDate.getTime());
        stream.writeUTF(myComment);
        writePathsInts(stream, myModifiedPaths);
        writePaths(stream, myAddedPaths);
        writePathsInts(stream, myDeletedPaths);
        writeMoved(stream, myMovedPaths);
    }


    private void loadChanges(){
        try{
            Changeset changeset = myWorkspace.getClient().getChangeset(myRevisionNumber);
            com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Change[] changes = changeset.getChanges();
            for(int i = 0; i < changes.length; i++){
                processChange(changeset.getChangesetID(), changes[i]);
            }
        } catch(Throwable e){
            AbstractVcsHelper.getInstance(myVcs.getProject()).showError(new VcsException(e.getMessage(), e),
                    TFSVcs.VCS_NAME);
        }
    }

    private void processChange(int changeset, com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Change change){
        ChangeType changeType = change.getChangeType();
        FilePath localPath = TFSFileUtil.getFilePath(change.getItem(), myWorkspace);
        if (localPath == null)
            return;
        if (changeType.containsAny(ChangeType.ADD.combine(ChangeType.UNDELETE).combine(ChangeType.BRANCH))){
            if (changeType.contains(ChangeType.ADD)){
                TFSVcs.assertTrue(changeType.contains(ChangeType.ENCODING));
                if (change.getItem().getItemType() == ItemType.FILE)
                    TFSVcs.assertTrue(changeType.contains(ChangeType.EDIT));
                else
                    TFSVcs.assertTrue(!changeType.contains(ChangeType.DELETE));
            }
            TFSVcs.assertTrue(!changeType.contains(ChangeType.DELETE));
            myAddedPaths.add(localPath);
            return;
        }
        if (changeType.contains(ChangeType.DELETE)){
            TFSVcs.assertTrue(changeType.toStringValues().length <= 3,
                    (new StringBuilder(Messages.getString("TFSChangeList.Unexpected")))
                            .append(changeType.toUIString(true)).toString());

            int previousCs = change.getItem().getChangeSetID() -1;
            myDeletedPaths.put(localPath, previousCs);
            return;
        }
        if(changeType.contains(ChangeType.RENAME)){
            if (change.getItem().getDeletionID() != 0x80000000 )
                return;
            Item item = getPreviousVersion(change.getItem(), changeset);
            FilePath originalPath = TFSFileUtil.getFilePath(item, myWorkspace);
            if (originalPath != null)
                myModifiedPaths.put(originalPath, Pair.create(localPath, item.getChangeSetID()));
            return;
        }
        if (changeType.containsAny(ChangeType.EDIT.combine(ChangeType.MERGE).combine(ChangeType.ENCODING))){
            int previousCs = change.getItem().getChangeSetID() -1;
            myModifiedPaths.put(localPath, previousCs);
            return;
        } else {
            TFSVcs.error((new StringBuilder()).append("Unknown change: ").append(changeType).append(" for item ")
                    .append(change.getItem().getServerItem()).toString());
        }

    }

    private Item getPreviousVersion(Item item, int changeset){
        ItemSpec[] itemSpecs = ItemSpec.fromStrings(new String[]{item.getServerItem()}, RecursionType.NONE);
        Check.isTrue(itemSpecs.length == 1, "one should be 1 item");
        ItemSpec itemSpec = itemSpecs[0];
        Changeset[] shortList =  myWorkspace.getClient().queryHistory(item.getServerItem(), new ChangesetVersionSpec(changeset), 0,
                RecursionType.NONE, null, new ChangesetVersionSpec(1), new ChangesetVersionSpec(item.getChangeSetID()),
                2, true, true, true, true);
        TFSVcs.assertTrue(shortList.length == 2);
        //TODO this probably isn't right
        return shortList[1].getChanges()[0].getItem();
    }


    private void readFromStream(DataInput stream) {
        try {
            myServerUri = new URI(stream.readUTF());
            myWorkspaceName = stream.readUTF();
            myRevisionNumber = stream.readInt();
            myAuthor = stream.readUTF();
            myDate = new Date(stream.readLong());
            myComment = stream.readUTF();
            readPathsInts(stream, myModifiedPaths);
            readPaths(stream, myAddedPaths);
            readPathsInts(stream, myDeletedPaths);
            readMoved(stream, myMovedPaths);
        } catch(IOException e) {
            AbstractVcsHelper.getInstance(myVcs.getProject()).showError(new VcsException(e), TFSVcs.VCS_NAME);
        } catch(URISyntaxException e) {
            AbstractVcsHelper.getInstance(myVcs.getProject()).showError(new VcsException(e), TFSVcs.VCS_NAME);
        }
    }

    private static void writePaths(DataOutput stream, Collection paths) throws IOException {
        stream.writeInt(paths.size());
        FilePath path;
        for(Iterator i$ = paths.iterator(); i$.hasNext(); writePath(stream, path))
            path = (FilePath)i$.next();

    }

    private static void writePathsInts(DataOutput stream, Map paths)
            throws IOException
    {
        stream.writeInt(paths.size());
        java.util.Map.Entry e;
        for(Iterator i$ = paths.entrySet().iterator(); i$.hasNext(); stream.writeInt(((Integer)e.getValue()).intValue()))
        {
            e = (java.util.Map.Entry)i$.next();
            writePath(stream, (FilePath)e.getKey());
        }

    }

    private static void writeMoved(DataOutput stream, Map paths)
            throws IOException
    {
        stream.writeInt(paths.size());
        java.util.Map.Entry e;
        for(Iterator i$ = paths.entrySet().iterator(); i$.hasNext(); stream.writeInt(((Integer)((Pair)e.getValue()).second).intValue()))
        {
            e = (java.util.Map.Entry)i$.next();
            writePath(stream, (FilePath)e.getKey());
            writePath(stream, (FilePath)((Pair)e.getValue()).first);
        }

    }

    private static void writePath(DataOutput stream, FilePath path)
            throws IOException
    {
        stream.writeUTF(path.getPath());
        //stream.writeBoolean(path.isDirectory());
    }

    private static void readPaths(DataInput stream, Collection paths)
            throws IOException
    {
        int count = stream.readInt();
        for(int i = 0; i < count; i++)
            paths.add(readPath(stream));

    }

    private static void readPathsInts(DataInput stream, Map paths)
            throws IOException
    {
        int count = stream.readInt();
        for(int i = 0; i < count; i++)
            paths.put(readPath(stream), Integer.valueOf(stream.readInt()));

    }

    private static void readMoved(DataInput stream, Map paths)
            throws IOException
    {
        int count = stream.readInt();
        for(int i = 0; i < count; i++)
            paths.put(readPath(stream), Pair.create(readPath(stream), Integer.valueOf(stream.readInt())));

    }

    private static FilePath readPath(DataInput stream)
            throws IOException
    {
        return TFSFileUtil.getFilePath(stream.readUTF());//, stream.readBoolean());
    }


}
